// Copyright 2017 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import fs from 'fs';

const kTimestampFactor = 1000;

function toTimestamp(microseconds) {
    return microseconds / kTimestampFactor;
}

function startOf(timestamp, time) {
    let result = toTimestamp(timestamp) - time;
    if (result < 0) {
        throw "start timestamp cannot be negative";
    }
    return result;
}

function BYTES(bytes, total) {
    let result = formatBytes(bytes);
    if (total !== undefined && total != 0) {
        result += PERCENT(bytes, total).padStart(5);
    }
    return result;
}

function PERCENT(value, total) {
    return Math.round(value / total * 100) + "%";
}

function formatBytes(bytes, digits = 2) {
    const units = ['B', 'KiB', 'MiB', 'GiB'];
    const divisor = 1024;
    let index = 0;
    while (index < units.length && bytes >= divisor) {
        index++;
        bytes /= divisor;
    }
    return bytes.toFixed(digits) + units[index];
}

class CompilationUnit {
    constructor() {
        this.isEval = false;
        this.isDeserialized = false;
        this.firstEventTimestamp = -1;
        this.firstParseEventTimestamp = -1;
        this.firstCompileEventTimestamp = -1;
        this.lastParseEventTimestamp = -1;
        this.lastEventTimestamp = -1;
        this.deserializationTimestamp = -1;

        this.preparseTimestamp = -1;
        this.parseTimestamp = -1;
        this.parse2Timestamp = -1;
        this.resolutionTimestamp = -1;
        this.compileTimestamp = -1;
        this.lazyCompileTimestamp = -1;
        this.executionTimestamp = -1;
        this.baselineTimestamp = -1;
        this.optimizeTimestamp = -1;

        this.deserializationDuration = -0.0;
        this.preparseDuration = -0.0;
        this.parseDuration = -0.0;
        this.parse2Duration = -0.0;
        this.resolutionDuration = -0.0;
        this.scopeResolutionDuration = -0.0;
        this.lazyCompileDuration = -0.0;
        this.compileDuration = -0.0;
        this.baselineDuration = -0.0;
        this.optimizeDuration = -0.0;

        this.ownBytes = -1;
        this.compilationCacheHits = [];
    }

    finalize() {
        this.firstEventTimestamp = this.timestampMin(
            this.deserializationTimestamp, this.parseTimestamp,
            this.preparseTimestamp, this.resolutionTimestamp,
            this.executionTimestamp);

        this.firstParseEventTimestamp = this.timestampMin(
            this.deserializationTimestamp, this.parseTimestamp,
            this.preparseTimestamp, this.resolutionTimestamp);

        this.firstCompileEventTimestamp = this.rawTimestampMin(
            this.deserializationTimestamp, this.compileTimestamp,
            this.baselineTimestamp, this.lazyCompileTimestamp);
        // Any excuted script needs to be compiled.
        if (this.hasBeenExecuted() &&
            (this.firstCompileEventTimestamp <= 0 ||
            this.executionTimestamp < this.firstCompileTimestamp)) {
        console.error('Compile < execution timestamp', this);
        }

        if (this.ownBytes < 0) console.error(this, 'Own bytes must be positive');
    }

    hasBeenExecuted() {
        return this.executionTimestamp > 0;
    }

    addCompilationCacheHit(timestamp) {
        this.compilationCacheHits.push(timestamp);
    }
        // Returns the smallest timestamp from the given list, ignoring
        // uninitialized (-1) values.
    rawTimestampMin(...timestamps) {
        timestamps = timestamps.length == 1 ? timestamps[0] : timestamps;
        let result = timestamps.reduce((min, item) => {
            return item == -1 ? min : (min == -1 ? item : Math.min(item, item));
        }, -1);
        return result;
    }
    timestampMin(...timestamps) {
        let result = this.rawTimestampMin(...timestamps);
        if (Number.isNaN(result) || result < 0) {
            return 0;
        }
        return result;
    }

    timestampMax(...timestamps) {
        timestamps = timestamps.length == 1 ? timestamps[0] : timestamps;
        let result = Math.max(...timestamps);
        if (Number.isNaN(result) || result < 0) {
            console.error(
                'Invalid timestamp max:', {result, timestamps, script: this});
            return 0;
        }
        return result;
    }
}

class Script extends CompilationUnit {
    constructor(id) {
        super();
        if (id === undefined || id <= 0) {
            throw new Error(`Invalid id=${id} for script`);
        }
        this.file = '';
        this.id = id;

        this.isNative = false;
        this.isBackgroundCompiled = false;
        this.isStreamingCompiled = false;

        this.funktions = [];
        this.metrics = new Map();
        this.maxNestingLevel = 0;

        this.width = 0;
        this.bytesTotal = -1;
        this.finalized = false;
        this.summary = '';
        this.source = '';
    }

    setFile(name) {
        this.file = name;
        this.isNative = name.startsWith('native ');
    }

    isEmpty() {
        return this.funktions.length === 0;
    }

    getFunktionAtStartPosition(start) {
        if (!this.isEval && start === 0) {
            throw 'position 0 is reserved for the script';
        }
        if (this.finalized) {
            return this.funktions.find(funktion => funktion.start == start);
        }
        return this.funktions[start];
    }

    // Return the innermost function at the given source position.
    getFunktionForPosition(position) {
        if (!this.finalized) throw 'Incomplete script';
        for (let i = this.funktions.length - 1; i >= 0; i--) {
            let funktion = this.funktions[i];
            if (funktion.containsPosition(position)) return funktion;
        }
        return undefined;
    }

    addMissingFunktions(list) {
        if (this.finalized) throw 'script is finalized!';
        list.forEach(fn => {
            if (this.funktions[fn.start] === undefined) {
                this.addFunktion(fn);
            }
        });
    }

    addFunktion(fn) {
        if (this.finalized) throw 'script is finalized!';
        if (fn.start === undefined) throw "Funktion has no start position";
        if (this.funktions[fn.start] !== undefined) {
        fn.print();
        throw "adding same function twice to script";
        }
        this.funktions[fn.start] = fn;
    }

    finalize() {
        this.finalized = true;
        // Compact funktions as we no longer need access via start byte position.
        this.funktions = this.funktions.filter(each => true);
        let parent = null;
        let maxNesting = 0;
        // Iterate over the Funktions in byte position order.
        this.funktions.forEach(fn => {
        fn.isEval = this.isEval;
        if (parent === null) {
            parent = fn;
        } else {
            // Walk up the nested chain of Funktions to find the parent.
            while (parent !== null && !fn.isNestedIn(parent)) {
                parent = parent.parent;
            }
            fn.parent = parent;
            if (parent) {
                maxNesting = Math.max(maxNesting, parent.addNestedFunktion(fn));
            }
            parent = fn;
        }
        });
        // Sanity checks to ensure that scripts are executed and parsed before any
        // of its funktions.
        let funktionFirstParseEventTimestamp = -1;
        // Second iteration step to finalize the funktions once the proper
        // hierarchy has been set up.
        this.funktions.forEach(fn => {
        fn.finalize();

        funktionFirstParseEventTimestamp = this.timestampMin(
            funktionFirstParseEventTimestamp, fn.firstParseEventTimestamp);

        this.lastParseEventTimestamp = this.timestampMax(
            this.lastParseEventTimestamp, fn.lastParseEventTimestamp);

        this.lastEventTimestamp =
            this.timestampMax(this.lastEventTimestamp, fn.lastEventTimestamp);
        });
        this.maxNestingLevel = maxNesting;

        // Initialize sizes.
        if (this.funktions.length == 0) {
            this.bytesTotal = this.ownBytes = 0;
            return;
        }
        let toplevelFunktionBytes = this.funktions.reduce(
            (bytes, each) => bytes + (each.isToplevel() ? each.getBytes() : 0), 0);
        if (this.isDeserialized || this.isEval || this.isStreamingCompiled) {
            if (this.getBytes() === -1) {
                this.bytesTotal = toplevelFunktionBytes;
            }
        }
        this.ownBytes = this.bytesTotal - toplevelFunktionBytes;
        // Initialize common properties.
        super.finalize();
    }

    print() {
        console.log(this.toString());
    }

    toString() {
        let str = `SCRIPT id=${this.id} file=${this.file}\n` +
        `functions[${this.funktions.length}]:`;
        this.funktions.forEach(fn => str += fn.toString());
        return str;
    }

    getBytes() {
        return this.bytesTotal;
    }

    getOwnBytes() {
        return this.ownBytes;
    }

    // Also see Funktion.prototype.getMetricBytes
    getMetricBytes(name) {
        if (name == 'lazyCompileTimestamp') return this.getOwnBytes();
        return this.getOwnBytes();
    }

    getMetricDuration(name) {
        return this[name];
    }

    forEach(fn) {
        fn(this);
        this.funktions.forEach(fn);
    }

    // Container helper for TotalScript / Script.
    getScripts() {
        return [this];
    }

    calculateMetrics(printSummary) {
        let log = (str) => this.summary += str + '\n';
        log(`SCRIPT: ${this.id}`);
        let all = this.funktions;
        if (all.length === 0) return;

        let nofFunktions = all.length;
        let ownBytesSum = list => {
            return list.reduce((bytes, each) => bytes + each.getOwnBytes(), 0)
        };

        let info = (name, funktions) => {
            let ownBytes = ownBytesSum(funktions);
            let nofPercent = Math.round(funktions.length / nofFunktions * 100);
            let value = (funktions.length + "#").padStart(7) +
                (nofPercent + "%").padStart(5) +
                BYTES(ownBytes, this.bytesTotal).padStart(16);
            log((`  - ${name}`).padEnd(20) + value);
            this.metrics.set(name + "-bytes", ownBytes);
            this.metrics.set(name + "-count", funktions.length);
            this.metrics.set(name + "-count-percent", nofPercent);
            this.metrics.set(name + "-bytes-percent",
                Math.round(ownBytes / this.bytesTotal * 100));
            };

        log(`  - file:         ${this.file}`);
        log('  - details:      ' +
            'isEval=' + this.isEval + ' deserialized=' + this.isDeserialized +
            ' streamed=' + this.isStreamingCompiled);
        log("    Category               Count           Bytes");
        info("scripts", this.getScripts());
        info("functions", all);
        info("toplevel fns", all.filter(each => each.isToplevel()));
        info('preparsed', all.filter(each => each.preparseDuration > 0));

        info('fully parsed', all.filter(each => each.parseDuration > 0));
        // info("fn parsed", all.filter(each => each.parse2Duration > 0));
        // info("resolved", all.filter(each => each.resolutionDuration > 0));
        info("executed", all.filter(each => each.executionTimestamp > 0));
        info('eval', all.filter(each => each.isEval));
        info("lazy compiled", all.filter(each => each.lazyCompileTimestamp > 0));
        info("eager compiled", all.filter(each => each.compileTimestamp > 0));

        info("baseline", all.filter(each => each.baselineTimestamp > 0));
        info("optimized", all.filter(each => each.optimizeTimestamp > 0));

        if (printSummary) console.log(this.summary);
        if (this.file.includes('.js')) {
            let realPercent = this.metrics.get('fully parsed-bytes-percent');
            console.log(`Actually use : ${realPercent}%, can use eager compile comment : ${realPercent>60}\n`);
        }
    }
}

class TotalScript extends Script {
    constructor() {
        super('all files', 'all files');
        this.scripts = [];
    }
    addAllFunktions(script) {
        // funktions is indexed by byte offset and as such not packed. Add every
        // Funktion one by one to keep this.funktions packed.
        script.funktions.forEach(fn => this.funktions.push(fn));
        this.scripts.push(script);
        this.bytesTotal += script.bytesTotal;
    }

    // Iterate over all Scripts and nested Funktions.
    forEach(fn) {
        this.scripts.forEach(script => script.forEach(fn));
    }

    getScripts() {
        return this.scripts;
    }
}

class Funktion extends CompilationUnit {
    constructor(name, start, end, script) {
        super();
        if (start < 0) throw `invalid start position: ${start}`;
        if (script.isEval) {
            if (end < start) throw 'invalid start end positions';
        } else {
            if (end <= 0) throw `invalid end position: ${end}`;
            if (end < start) throw 'invalid start end positions';
            if (end == start) verboseLogWarn("Possibly invalid start/end position")
        }

        this.name = name;
        this.start = start;
        this.end = end;
        this.script = script;
        this.parent = null;
        this.nested = [];
        this.nestingLevel = 0;

        if (script) this.script.addFunktion(this);
    }

    finalize() {
        this.lastParseEventTimestamp = Math.max(
            this.preparseTimestamp + this.preparseDuration,
            this.parseTimestamp + this.parseDuration,
            this.resolutionTimestamp + this.resolutionDuration);
        if (!(this.lastParseEventTimestamp > 0)) this.lastParseEventTimestamp = 0;

        this.lastEventTimestamp =
            Math.max(this.lastParseEventTimestamp, this.executionTimestamp);
        if (!(this.lastEventTimestamp > 0)) this.lastEventTimestamp = 0;

        this.ownBytes = this.nested.reduce(
            (bytes, each) => bytes - each.getBytes(), this.getBytes());

        super.finalize();
    }

    getMetricBytes(name) {
        if (name == 'lazyCompileTimestamp') return this.getOwnBytes();
        return this.getOwnBytes();
    }

    getMetricDuration(name) {
        if (name in kNoTimeMetrics) return 0;
        return this[name];
    }

    isNestedIn(funktion) {
        if (this.script != funktion.script) throw "Incompatible script";
        return funktion.start < this.start && this.end <= funktion.end;
    }

    isToplevel() {
        return this.parent === null;
    }

    containsPosition(position) {
        return this.start <= position && position <= this.end;
    }

    accumulateNestingLevel(accumulator) {
        let value = accumulator[this.nestingLevel] || 0;
        accumulator[this.nestingLevel] = value + this.getOwnBytes();
    }

    addNestedFunktion(child) {
        if (this.script != child.script) throw "Incompatible script";
        if (child == null) throw "Nesting non child";
        this.nested.push(child);
        if (this.nested.length > 1) {
            // Make sure the nested children don't overlap and have been inserted in
            // byte start position order.
            let last = this.nested[this.nested.length - 2];
            if (last.end > child.start || last.start > child.start ||
                last.end > child.end || last.start > child.end) {
                throw "Wrongly nested child added";
            }
        }
        child.nestingLevel = this.nestingLevel + 1;
        return child.nestingLevel;
    }

    getBytes() {
        return this.end - this.start;
    }

    getOwnBytes() {
        return this.ownBytes;
    }

    didMetricChange(time, delta, name) {
        let value = this[name + 'Timestamp'];
        return (time - delta) <= value && value <= (time + delta);
    }

    print() {
        console.log(this.toString());
    }

    toString(details = true) {
        let result = `function${this.name ? ` ${this.name}` : ''}` +
            `() range=${this.start}-${this.end}`;
        if (details) result += ` script=${this.script ? this.script.id : 'X'}`;
        return result;
    }
}

class ParseProcessor {
    constructor() {
        this.functionEventDispatchTable_ = {
            // Avoid accidential leaking of __proto__ properties and force this object
            // to be in dictionary-mode
            __proto__: null,
            'full-parse': this.processFull.bind(this),
            'parse-function': this.processParseFunction.bind(this),
            'parse': this.processParseFunction.bind(this),
            'parse-script': this.processParseScript.bind(this),
            'parse-eval': this.processParseEval.bind(this),
            'preparse-no-resolution': this.processPreparseNoResolution.bind(this),
            'preparse-resolution': this.processPreparseResolution.bind(this),
            'first-execution': this.processFirstExecution.bind(this),
            'first-execution-BUILTIN': this.processFirstExecution.bind(this),
            'interpreter': this.processCompile.bind(this),
            'interpreter-eval': this.processCompileEval.bind(this),
            'baseline': this.processBaselineLazy.bind(this),
            'baseline-lazy': this.processBaselineLazy.bind(this),
            'optimize-lazy': this.processOptimizeLazy.bind(this),
            'turbofan': this.processOptimizeLazy.bind(this),
            'deserialize': this.processDeserialize.bind(this),  
        };

        this.idToScript = new Map();
        this.fileToScript = new Map();
        this.nameToFunction = new Map();
        this.scripts = [];
        this.totalScript = new TotalScript();
        this.firstEventTimestamp = -1;
        this.lastParseEventTimestamp = -1;
        this.lastEventTimestamp = -1;
    }

    print() {
        console.log("scripts:");
        this.idToScript.forEach(script => script.print());
    }

  processFunctionEvent(
      eventName, scriptId, startPosition, endPosition, duration, timestamp,
      functionName) {
    let handlerFn = this.functionEventDispatchTable_[eventName];
    if (handlerFn === undefined) {
      console.error(`Couldn't find handler for function event:${eventName}`);
    }
    handlerFn(
        scriptId, startPosition, endPosition, duration, timestamp,
        functionName);
  }
  addEntry(entry) {
    this.entries.push(entry);
  }

  lookupScript(id) {
    return this.idToScript.get(id);
  }

  getOrCreateFunction(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
    if (scriptId == -1) {
      return this.lookupFunktionByRange(startPosition, endPosition);
    }
    let script = this.lookupScript(scriptId);
    let funktion = script.getFunktionAtStartPosition(startPosition);
    if (funktion === undefined) {
      funktion = new Funktion(functionName, startPosition, endPosition, script);
    }
    return funktion;
  }

  // Iterates over all functions and tries to find matching ones.
  lookupFunktionsByRange(start, end) {
    let results = [];
    this.idToScript.forEach(script => {
      script.forEach(funktion => {
        if (funktion.startPosition == start && funktion.endPosition == end) {
          results.push(funktion);
        }
      });
    });
    return results;
  }
  lookupFunktionByRange(start, end) {
    let results = this.lookupFunktionsByRange(start, end);
    if (results.length != 1) throw "Could not find unique function by range";
    return results[0];
  }
  processScriptEvent(eventName, scriptId, timestamp) {
    let script = this.idToScript.get(scriptId);
    switch (eventName) {
      case 'create':
      case 'reserve-id':
      case 'deserialize': {
        if (script !== undefined) return;
        script = new Script(scriptId);
        this.idToScript.set(scriptId, script);
        if (eventName == 'deserialize') {
          script.deserializationTimestamp = toTimestamp(timestamp);
        }
        return;
      }
      case 'background-compile':
        if (script.isBackgroundCompiled) {
          throw 'Cannot background-compile twice';
        }
        script.isBackgroundCompiled = true;
        // TODO(cbruni): remove once backwards compatibility is no longer needed.
        script.isStreamingCompiled = true;
        // TODO(cbruni): fix parse events for background compilation scripts
        script.preparseTimestamp = toTimestamp(timestamp);
        return;
      case 'streaming-compile':
        if (script.isStreamingCompiled) throw 'Cannot stream-compile twice';
        // TODO(cbruni): remove once backwards compatibility is no longer needed.
        script.isBackgroundCompiled = true;
        script.isStreamingCompiled = true;
        // TODO(cbruni): fix parse events for background compilation scripts
        script.preparseTimestamp = toTimestamp(timestamp);
        return;
      default:
        verboseLogWarn(`Unhandled script event: ${eventName}`);
    }
  }

    processScriptDetails(scriptId, file, startLine, startColumn, size) {
    let script = this.lookupScript(scriptId);
    script.setFile(file);
  }

  processScriptSource(scriptId, url, source) {
    let script = this.lookupScript(scriptId);
    script.source = source;
  }

  processParseEval(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
    if (startPosition != 0 && startPosition != -1) {
      console.error('Invalid start position for parse-eval', arguments);
    }
    let script = this.processParseScript(...arguments);
    script.isEval = true;
  }

  processFull(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
    if (startPosition == 0) {
      // This should only happen for eval.
      let script = this.lookupScript(scriptId);
      script.isEval = true;
      return;
    }
    let funktion = this.getOrCreateFunction(...arguments);
    // TODO(cbruni): this should never happen, emit differen event from the
    // parser.
    if (funktion.parseTimestamp > 0) return;
    funktion.parseTimestamp = startOf(timestamp, duration);
    funktion.parseDuration = duration;
  }
  processParseFunction(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
    let funktion = this.getOrCreateFunction(...arguments);
    funktion.parseTimestamp = startOf(timestamp, duration);
    funktion.parseDuration = duration;
  }

  processParseScript(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
    // TODO timestamp and duration
    let script = this.lookupScript(scriptId);
    let ts = startOf(timestamp, duration);
    script.parseTimestamp = ts;
    script.parseDuration = duration;
    return script;
  }

  processPreparseResolution(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
    let funktion = this.getOrCreateFunction(...arguments);
    // TODO(cbruni): this should never happen, emit different event from the
    // parser.
    if (funktion.resolutionTimestamp > 0) return;
    funktion.resolutionTimestamp = startOf(timestamp, duration);
    funktion.resolutionDuration = duration;
  }
  processPreparseNoResolution(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
    let funktion = this.getOrCreateFunction(...arguments);
    funktion.preparseTimestamp = startOf(timestamp, duration);
    funktion.preparseDuration = duration;
  }

  processFirstExecution(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
    let script = this.lookupScript(scriptId);
    if (startPosition === 0) {
      // undefined = eval fn execution
      if (script) {
        script.executionTimestamp = toTimestamp(timestamp);
      }
    } else {
      let funktion = script.getFunktionAtStartPosition(startPosition);
      if (funktion) {
        funktion.executionTimestamp = toTimestamp(timestamp);
      } else {
        // TODO(cbruni): handle funktions from  compilation-cache hits.
      }
    }
  }

  processCompileLazy(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
    let funktion = this.getOrCreateFunction(...arguments);
    funktion.lazyCompileTimestamp = startOf(timestamp, duration);
    funktion.lazyCompileDuration = duration;
  }

  processCompile(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
    let script = this.lookupScript(scriptId);
    if (startPosition === 0) {
      script.compileTimestamp = startOf(timestamp, duration);
      script.compileDuration = duration;
      script.bytesTotal = endPosition;
      return script;
    } else {
      let funktion = script.getFunktionAtStartPosition(startPosition);
      if (funktion === undefined) {
        // This should not happen since any funktion has to be parsed first.
        return;
      }
      funktion.compileTimestamp = startOf(timestamp, duration);
      funktion.compileDuration = duration;
      return funktion;
    }
  }

  processCompileEval(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
    let compilationUnit = this.processCompile(...arguments);
    compilationUnit.isEval = true;
  }

  processBaselineLazy(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
    let compilationUnit = this.lookupScript(scriptId);
    if (startPosition > 0) {
      compilationUnit =
          compilationUnit.getFunktionAtStartPosition(startPosition);
      if (compilationUnit === undefined) {
        // This should not happen since any funktion has to be parsed first.
        console.error('processBaselineLazy funktion not found', ...arguments);
        return;
      }
    }
    compilationUnit.baselineTimestamp = startOf(timestamp, duration);
    compilationUnit.baselineDuration = duration;
  }

  processOptimizeLazy(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
    let compilationUnit = this.lookupScript(scriptId);
    if (startPosition > 0) {
      compilationUnit =
          compilationUnit.getFunktionAtStartPosition(startPosition);
      if (compilationUnit === undefined) {
        // This should not happen since any funktion has to be parsed first.
        console.error('processOptimizeLazy funktion not found', ...arguments);
        return;
      }
    }
    compilationUnit.optimizeTimestamp = startOf(timestamp, duration);
    compilationUnit.optimizeDuration = duration;
  }

  processDeserialize(
      scriptId, startPosition, endPosition, duration, timestamp, functionName) {
    let compilationUnit = this.lookupScript(scriptId);
    if (startPosition === 0) {
      compilationUnit.bytesTotal = endPosition;
    } else {
      compilationUnit = this.getOrCreateFunction(...arguments);
    }
    compilationUnit.deserializationTimestamp = startOf(timestamp, duration);
    compilationUnit.deserializationDuration = duration;
    compilationUnit.isDeserialized = true;
  }

    processCompilationCacheEvent(
      eventType, cacheType, scriptId, startPosition, endPosition, timestamp) {
    if (eventType !== 'hit') return;
    let compilationUnit = this.lookupScript(scriptId);
    if (startPosition > 0) {
      compilationUnit =
          compilationUnit.getFunktionAtStartPosition(startPosition);
    }
    compilationUnit.addCompilationCacheHit(toTimestamp(timestamp));
  }

  getFunctionStatistics() {
    this.scripts = Array.from(this.idToScript.values()).filter(each => !each.isNative);
    if (this.scripts.length === 0) {
        verboseLogWarn(`Could not find any scripts!`);
        return false;
    }

    this.scripts.forEach(script => {
        script.finalize();
        script.calculateMetrics(true);
    });
    this.firstEventTimestamp = this.totalScript.timestampMin(
        this.scripts.map(each => each.firstEventTimestamp));
    this.lastParseEventTimestamp = this.totalScript.timestampMax(
        this.scripts.map(each => each.lastParseEventTimestamp));
    this.lastEventTimestamp = this.totalScript.timestampMax(
        this.scripts.map(each => each.lastEventTimestamp));
  }
}

function verboseLog(message, verbose = false) {
    if (verbose) {
        console.log(message);
    }
}

function verboseLogWarn(message, verbose = false) {
    if (verbose) {
        console.warn(message);
    }
}

function main() {
    const args = process.argv.slice(2);
    let logFilePath;
    let verbose = false;

    for (let i = 0; i < args.length; i++) {
        if (args[i] === '--verbose' || args[i] === '-v') {
            verbose = true;
            args.splice(i, 1);
            i--;
        } else if (args[i] === '--help' || args[i] === '-h') {
            console.log('用法: node compile_tools.js [日志文件] [选项]');
            console.log('选项:');
            console.log('   -v, --verbose     显示详细输出信息');
            console.log('   -h, --help        显示帮助信息');
            process.exit(0);
        } 
    }

    if (args.length > 0) {
        logFilePath = args[0];
    } else {
        console.log('未指定日志文件！！！');
        process.exit(0);
    }

    verboseLog(`正在解析 v8 日志文件， ${logFilePath}`, verbose);

    if (!fs.existsSync(logFilePath)) {
        console.error(`错误： 文件 ${logFilePath} 不存在！`);
        process.exit(0);
    }

    try {
        const logContent = fs.readFileSync(logFilePath, 'utf8');
        const lines = logContent.split('\n');

        const processor = new ParseProcessor();
        verboseLog('开始解析日志文件...', verbose);

        let processedLines = 0;
        let scriptCount = 0;
        let functionEventCount = 0;
        let scriptDetailsCount = 0;
        let compilationCacheCount = 0;

        lines.forEach((line, index) => {
            line = line.trim();
            if (!line || line.startsWith('v8-version') ||
                line.startsWith('v8-platform') ||
                line.startsWith('heap-') ||
                line.startsWith('new,') ||
                line.startsWith('delete,')) {
                return;
            }

            processedLines++;

            try {
                const parts = line.split(',');
                const eventType = parts[0];

                switch (eventType) {
                    case 'script':
                        if (parts.length >= 3) {
                            processor.processScriptEvent(parts[1], parseInt(parts[2]), parts[3]);
                            scriptCount++;
                        }
                        break;
                    case 'script-details':
                        if (parts.length >= 5) {
                            processor.processScriptDetails(
                                parseInt(parts[1]),
                                parts[2] || '',
                                parseInt(parts[3]) || 0,
                                parseInt(parts[4]) || 0,
                                parts[5] || ''
                            );
                            scriptDetailsCount++;
                        }
                        break;
                    case 'function':
                        if (parts.length >= 7) {
                            processor.processFunctionEvent(
                                 parts[1],
                                 parseInt(parts[2]),
                                 parseInt(parts[3]),
                                 parseInt(parts[4]),
                                 parseFloat(parts[5]),
                                 parseFloat(parts[6]),
                                 parts[7] || ''
                            );
                            functionEventCount++;
                        }
                        break;
                    case 'compilation-cache':
                        if (parts.length >= 6) {
                            processor.processCompilationCacheEvent(
                                parts[1],
                                parts[2] || '',
                                parseInt(parts[3]),
                                parseInt(parts[4]),
                                parseInt(parts[5]),
                                parts[6] ? parseFloat(parts[6]) : 0
                            );
                            compilationCacheCount++;
                        }
                        break;
                }
            } catch (error) {
                verboseLogWarn(`处理第${index + 1}行时出错： ${error.message}`, verbose);
                verboseLogWarn(`原始行${line}`, verbose);
            }
        });

        verboseLog('\n解析完成！', verbose);

        if (verbose) {
            verboseLog(`总处理行数 ：${processedLines}`, verbose);
            verboseLog(`脚本事件数 ：${scriptCount}`, verbose);
            verboseLog(`脚本详细事件数 ：${scriptDetailsCount}`, verbose);
            verboseLog(`函数事件数 ：${functionEventCount}`, verbose);
            verboseLog(`编译缓存事件数 ：${compilationCacheCount}`, verbose);
        }

        console.log('\n' + '='.repeat(50));
        console.log('V8 Parse Processor result');
        console.log('='.repeat(50));
        processor.getFunctionStatistics();
    } catch (error) {
        console.error('处理文件时出错：', error.message);
        process.exit(1);
    }
}

if (typeof process !== 'undefined' && process.release.name === 'node') {
    main();
}

